"""CKI's extended KCIDB schema validation.

Provides `validate_kcidb(data)` to validate a dict against the CKI's schema,
which raises a `ValidationError` in case of problems.
"""
import argparse
import json
import pathlib
import pkgutil
from warnings import warn

import jsonschema
from kcidb_io import schema

from cki_lib import misc
from cki_lib import yaml
from cki_lib.logger import get_logger

LOGGER = get_logger(__name__)

CKI_SCHEMA = yaml.load(contents=pkgutil.get_data(__name__, 'schema.yaml'))

CONSUMER_KCIDB_SCHEMA = schema.V4_2  # pylint: disable=invalid-name
PRODUCER_KCIDB_SCHEMA = schema.V4_2  # pylint: disable=invalid-name

KCIDB_STATUS_CHOICES = misc.get_nested_key(
    PRODUCER_KCIDB_SCHEMA.json, '$defs/status/enum',
    default=["FAIL", "ERROR", "MISS", "PASS", "DONE", "SKIP"])


def sanitize_kcidb_status(result: str) -> str:
    """Sanitize the result from tests and subtests.

    Beaker/Restraint/subtests have some "interesting" task states that are not
    available in KCIDB. Sanitize the known ones, and fall back to ERROR otherwise.
    """
    status = result.upper()
    if status in {'WARN', 'WARN/ABORTED'}:
        return "ERROR"
    if status == 'PANIC':
        return "FAIL"
    if status not in KCIDB_STATUS_CHOICES:
        LOGGER.warning("Parsing invalid status %r into ERROR", status)
        return "ERROR"

    return status


def sanitize_all_kcidb_status(data) -> None:
    """Sanitize the whole payload, converting known invalid values into valid.

    NOTE: The operation is done inplace.
    """
    for test in data.get("tests", []):
        if status := test.get("status"):
            test["status"] = sanitize_kcidb_status(status)

        if results := misc.get_nested_key(test, "misc/results", default=[]):
            status_priority = {status: -idx for idx, status in enumerate(KCIDB_STATUS_CHOICES)}
            status_priority[None] = -99
            max_result_status = None

            for result in results:
                if status := result.get("status"):
                    result["status"] = sanitize_kcidb_status(status)

                    if status_priority[result["status"]] > status_priority[max_result_status]:
                        max_result_status = result["status"]

            if test.get("status") != max_result_status:
                LOGGER.error("Mismatching Test.status and greatest TestResult.status (%r != %r)",
                             test.get("status"), max_result_status)
                if status_priority[test.get("status")] < status_priority[max_result_status]:
                    LOGGER.info("Setting Test.status to greatest TestResult.status")
                    test["status"] = max_result_status


def _validate_kcidb(data, *, kcidb_schema=CONSUMER_KCIDB_SCHEMA):
    """Validate kcidb with additional checks."""
    try:
        kcidb_schema.validate(data)
    except jsonschema.exceptions.ValidationError as validation_error:
        raise yaml.ValidationError("Failed to validate against the KCIDB schema.",
                                   validation_error) from None


def _validate_origins(data):
    """Check if objects in the upstream schema with origin start with it."""
    errors = []
    for checkout in data.get('checkouts', []):
        if not checkout['id'].startswith(checkout['origin'] + ':'):
            errors.append(
                f"checkout id ({checkout['id']}) does not match "
                f"origin ({checkout['origin']}) constraint"
            )

    for build in data.get('builds', []):
        if not build['id'].startswith(build['origin'] + ':'):
            errors.append(
                f"build id ({build['id']}) does not match "
                f"origin ({build['origin']}) constraint"
            )

    for test in data.get('tests', []):
        if not test['id'].startswith(test['origin'] + ':'):
            errors.append(
                f"test id ({test['id']}) does not match "
                f"origin ({test['origin']}) constraint"
            )
    if errors:
        raise yaml.ValidationError("\n".join(errors))


def validate_kcidb(data):
    """Validate kcidb with additional checks.

    Deprecated, use `cki_lib.kcidb.validate_extended_kcidb_schema` instead.
    """
    warn(
        ("Function `cki_lib.kcidb.validate.validate_kcidb` will be deprecated soon,"
         " please use `cki_lib.kcidb.validate_extended_kcidb_schema`."),
        DeprecationWarning, stacklevel=2)
    _validate_kcidb(data)
    _validate_origins(data)


def _validate_cki_schema(data):
    """Validate data against the CKI's extended KCIDB schema, which constraints the misc fields."""
    yaml.validate(
        instance=data,
        schema=CKI_SCHEMA,
        format_checker=jsonschema.Draft7Validator.FORMAT_CHECKER,
        error_str="Failed to validate against the CKI's extended KCIDB schema."
    )


def validate_extended_kcidb_schema(
    data: dict, *, raise_for_cki: bool = True, kcidb_schema=CONSUMER_KCIDB_SCHEMA
):
    """
    Validate data against the KCIDB schema with CKI's extension.

    Args:
        raise_for_cki: whether it should raise yaml.ValidationError on CKI schema for "misc" fields
        kcidb_schema: which KCIDB schema to validate against. Defaults to CONSUMER_KCIDB_SCHEMA.

    Raises:
        yaml.ValidationError: if data is not a valid KCIDB dict or the ID fields don't match their
            corresponding origin. If `raise_for_cki` is enabled, may also happen if the "misc"
            fields in the data are not compliant with CKI schema.
    """
    sanitize_all_kcidb_status(data)

    _validate_kcidb(data, kcidb_schema=kcidb_schema)
    _validate_origins(data)
    try:
        _validate_cki_schema(data)
    except yaml.ValidationError:
        # Temporary try-except to avoid crashing dependents failing to validate against the schema.
        # This can be removed once nothing is using validate()
        if raise_for_cki:
            raise
        LOGGER.exception("Failure to validate against the schema may crash in the future.")


def validate(data):
    """Validate data against the KCIDB schema with CKI's extension.

    Deprecated, use `cki_lib.kcidb.validate_extended_kcidb_schema` instead.
    """
    warn(
        ("Function `cki_lib.kcidb.validate.validate` will be deprecated soon,"
         " please use `cki_lib.kcidb.validate_extended_kcidb_schema`."),
        DeprecationWarning, stacklevel=2)

    validate_extended_kcidb_schema(data, raise_for_cki=False)


def main(argv=None):
    """Run the command line interface."""
    parser = argparse.ArgumentParser()
    parser.add_argument('file')

    args = parser.parse_args(argv)

    validate_extended_kcidb_schema(json.loads(pathlib.Path(args.file).read_text(encoding='utf8')))


if __name__ == "__main__":
    main()
